#include <iostream>
#include <sstream>
#include <ruby.h>
#include "m3g/m3g.hpp"
#include "ruby-m3g.hpp"
using namespace m3g;
using namespace std;

namespace {
    struct PixelsAccessor {
        Image2D* image;
    };
    VALUE rb_cImage2D_PixelsAccessor;
}


void ruby_Image2D_mark (Image2D* ptr)
{
    vector<Object3D*> objs;
    extract_all_references (ptr, objs);
    for (int i = 0; i < (int)objs.size(); i++) {
        VALUE val_obj = (VALUE)objs[i]->getExportedEntity();
        if (val_obj)
            rb_gc_mark (val_obj);
    }
}

void ruby_Image2D_free (Image2D* ptr)
{
    if (valid_ptr((unsigned char*)ptr, sizeof(*ptr))) {
        __TRY__;
        ptr->~Image2D ();
        __CATCH__;
    }
    ruby_xfree (ptr);
}

VALUE ruby_Image2D_allocate (VALUE self)
{
    void* p = ruby_xmalloc (sizeof(Image2D));
    return Data_Wrap_Struct (self, ruby_Image2D_mark, ruby_Image2D_free, p);
}

VALUE ruby_Image2D_initialize (int argc, VALUE* argv, VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);

    VALUE val_arg1, val_arg2, val_arg3, val_arg4, val_arg5;
    int num = rb_scan_args (argc, argv, "32", &val_arg1, &val_arg2, &val_arg3, &val_arg4, &val_arg5);

    switch (num) {
    case 3: {
        int format = NUM2INT (val_arg1);
        int width  = NUM2INT (val_arg2);
        int height = NUM2INT (val_arg3);
        __TRY__;
        new (p) Image2D (format, width, height);
        __CATCH__;
        break;
    }
    case 4: {
        rb_check_type (val_arg4, T_STRING);
        int   format = NUM2INT (val_arg1);
        int   width  = NUM2INT (val_arg2);
        int   height = NUM2INT (val_arg3);
        char* image  = StringValuePtr (val_arg4);
        __TRY__;
        new (p) Image2D (format, width, height, (void*)image);
        __CATCH__;
        break;
    }
    case 5: {
        rb_check_type (val_arg4, T_STRING);
        rb_check_type (val_arg5, T_STRING);

        int   format  = NUM2INT (val_arg1);
        int   width   = NUM2INT (val_arg2);
        int   height  = NUM2INT (val_arg3);
        char* image   = StringValuePtr (val_arg4);
        char* palette = StringValuePtr (val_arg5);
        __TRY__;
        new (p) Image2D (format, width, height, (unsigned char*)image, (void*)palette);
        __CATCH__;
        break;
    }
    default: {
        rb_raise (rb_eArgError, "wrong number of arguments %d (expected %d)", num);
    }
    }

    p->setExportedEntity ((void*)self);
    return self;
}

VALUE ruby_Image2D_get_format (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    int format;
    __TRY__;
    format = p->getFormat ();
    __CATCH__;
    return INT2NUM(format);
}

VALUE ruby_Image2D_get_height (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    int height;
    __TRY__;
    height = p->getHeight ();
    __CATCH__;
    return INT2NUM(height);
}

VALUE ruby_Image2D_get_width (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    int width;
    __TRY__;
    width = p->getWidth ();
    __CATCH__;
    return INT2NUM(width);
}

VALUE ruby_Image2D_get_pixels (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    PixelsAccessor* accessor;
    VALUE       val_accessor = Data_Make_Struct (rb_cImage2D_PixelsAccessor, PixelsAccessor, 0, -1, accessor);
    accessor->image = p;
    return val_accessor;
}

VALUE ruby_Image2D_write_png (VALUE self, VALUE val_name)
{
    rb_check_type (val_name, T_STRING);

    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    char* name = StringValuePtr (val_name);
    __TRY__;
    p->writePNG (name);
    __CATCH__;
    return Qnil;
}

VALUE ruby_Image2D_is_mutable (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    bool mutabl;
    __TRY__;
    mutabl = p->isMutable();
    __CATCH__;
    return mutabl ? Qtrue : Qfalse;
}

VALUE ruby_Image2D_print (VALUE self)
{
    Image2D* p;
    Data_Get_Struct (self, Image2D, p);
    ostringstream out;
    __TRY__;
    p->print (out);
    __CATCH__;
    return rb_str_new2 (out.str().c_str());
}

/**
 * Image2D:: PixelsAccessor
 */
VALUE ruby_Image2D_PixelsAccessor_set (VALUE self, VALUE val_x, VALUE val_y, VALUE val_width, VALUE val_height, VALUE val_image)
{
    rb_check_type (val_image, T_STRING);
    PixelsAccessor* p;
    Data_Get_Struct (self, PixelsAccessor, p);
    int   x      = NUM2INT (val_x);
    int   y      = NUM2INT (val_y);
    int   width  = NUM2INT (val_width);
    int   height = NUM2INT (val_height);
    char* image  = StringValuePtr (val_image);
    __TRY__;
    p->image->set (x, y, width, height, (void*)image);
    __CATCH__;
    return Qnil;
}

/**
 *
 */
void register_Image2D ()
{
    // Image2D
    rb_cImage2D = rb_define_class_under (rb_mM3G, "Image2D",             rb_cObject3D);

    rb_define_const (rb_cImage2D, "ALPHA",           INT2NUM(Image2D::ALPHA));
    rb_define_const (rb_cImage2D, "LUMINANCE",       INT2NUM(Image2D::LUMINANCE));
    rb_define_const (rb_cImage2D, "LUMINANCE_ALPHA", INT2NUM(Image2D::LUMINANCE_ALPHA));
    rb_define_const (rb_cImage2D, "RGB",             INT2NUM(Image2D::RGB));
    rb_define_const (rb_cImage2D, "RGBA",            INT2NUM(Image2D::RGBA));

    rb_define_alloc_func     (rb_cImage2D, ruby_Image2D_allocate);
    rb_define_private_method (rb_cImage2D, "initialize", (VALUE(*)(...))ruby_Image2D_initialize, -1);

    rb_define_method (rb_cImage2D, "format",    (VALUE(*)(...))ruby_Image2D_get_format, 0);
    rb_define_method (rb_cImage2D, "height",    (VALUE(*)(...))ruby_Image2D_get_height, 0);
    rb_define_method (rb_cImage2D, "width",     (VALUE(*)(...))ruby_Image2D_get_width,  0);
    rb_define_method (rb_cImage2D, "pixels",    (VALUE(*)(...))ruby_Image2D_get_pixels, 0);
    rb_define_method (rb_cImage2D, "write_png", (VALUE(*)(...))ruby_Image2D_write_png,  1);
    rb_define_method (rb_cImage2D, "mutable?",  (VALUE(*)(...))ruby_Image2D_is_mutable, 0);
    rb_define_method (rb_cImage2D, "to_s",      (VALUE(*)(...))ruby_Image2D_print     , 0);

    // Image2D:: PixelsAccessor
    rb_cImage2D_PixelsAccessor  = rb_define_class_under (rb_cImage2D, "PixelsAccessor", rb_cObject);

    rb_define_method (rb_cImage2D_PixelsAccessor, "[]=",        (VALUE(*)(...))ruby_Image2D_PixelsAccessor_set,    5);
}
